/* eslint-disable max-statements */ import type { MDXComponents } from 'mdx/types'; import type { GetStaticPaths, GetStaticProps } from 'next'; import dynamic from 'next/dynamic'; import Head from 'next/head'; import NextImage, { type ImageProps as NextImageProps } from 'next/image'; import { useRouter } from 'next/router'; import Script from 'next/script'; import type { ComponentType, HTMLAttributes } from 'react'; import { useIntl } from 'react-intl'; import { Code, Gallery, getLayout, Link, Overview, PageLayout, Sharing, SocialLink, Spinner, Heading, List, ListItem, Figure, type MetaItemData, type MetaValues, } from '../../components'; import styles from '../../styles/pages/project.module.scss'; import type { NextPageWithLayout, ProjectPreview, Repos } from '../../types'; import { ROUTES } from '../../utils/constants'; import { getFormattedDate, getSchemaJson, getSinglePageSchema, getWebPageSchema, } from '../../utils/helpers'; import { getProjectData, getProjectFilenames, loadTranslation, type Messages, } from '../../utils/helpers/server'; import { useBreadcrumb, useGithubApi, useSettings } from '../../utils/hooks'; const BorderedImage = (props: NextImageProps) => (
); const H1 = ({ children = '', ...props }: HTMLAttributes) => ( {children} ); const H2 = ({ children = '', ...props }: HTMLAttributes) => ( {children} ); const H3 = ({ children = '', ...props }: HTMLAttributes) => ( {children} ); const H4 = ({ children = '', ...props }: HTMLAttributes) => ( {children} ); const H5 = ({ children = '', ...props }: HTMLAttributes) => ( {children} ); const H6 = ({ children = '', ...props }: HTMLAttributes) => ( {children} ); const OrderedList = ({ children, ...props }: HTMLAttributes) => ( {children} ); const UnorderedList = ({ children, ...props }: HTMLAttributes) => ( {children} ); const components: MDXComponents = { Code, Gallery, h1: H1, h2: H2, h3: H3, h4: H4, h5: H5, h6: H6, Image: BorderedImage, li: ListItem, Link, ol: OrderedList, ul: UnorderedList, }; type ProjectPageProps = { project: ProjectPreview; translation: Messages; }; /** * Project page. */ const ProjectPage: NextPageWithLayout = ({ project }) => { const { id, intro, meta, title } = project; const { cover, dates, license, repos, seo, technologies } = meta; const intl = useIntl(); const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title, url: `${ROUTES.PROJECTS}/${id}`, }); const ProjectContent: ComponentType = dynamic( async () => import(`../../content/projects/${id}.mdx`), { loading: () => , } ); const { website } = useSettings(); const { asPath } = useRouter(); const page = { title: `${seo.title} - ${website.name}`, url: `${website.url}${asPath}`, }; /** * Retrieve a formatted date (and time). * * @param {string} date - A date string. * @returns {JSX.Element} The formatted date wrapped in a time element. */ const getDate = (date: string): JSX.Element => { const isoDate = new Date(`${date}`).toISOString(); return ; }; const headerMeta: (MetaItemData | undefined)[] = [ { id: 'publication-date', label: intl.formatMessage({ defaultMessage: 'Published on:', description: 'ProjectsPage: publication date label', id: 'HxZvY4', }), value: getDate(dates.publication), }, dates.update && dates.update !== dates.publication ? { id: 'update-date', label: intl.formatMessage({ defaultMessage: 'Updated on:', description: 'ProjectsPage: update date label', id: 'wQrvgw', }), value: getDate(dates.update), } : undefined, ]; const filteredHeaderMeta = headerMeta.filter( (item): item is MetaItemData => !!item ); /** * Retrieve the repositories links. * * @param {Repos} repositories - A repositories object. * @returns {MetaValues[]} - An array of meta values. */ const getReposLinks = (repositories: Repos): MetaValues[] => { const links: MetaValues[] = []; const githubLabel = intl.formatMessage({ defaultMessage: 'Github profile', description: 'ProjectsPage: Github profile link', id: 'Nx8Jo5', }); const gitlabLabel = intl.formatMessage({ defaultMessage: 'Gitlab profile', description: 'ProjectsPage: Gitlab profile link', id: 'sECHDg', }); if (repositories.github) links.push({ id: 'github', value: ( ), }); if (repositories.gitlab) links.push({ id: 'gitlab', value: ( ), }); return links; }; const loadingRepoPopularity = intl.formatMessage({ defaultMessage: 'Loading the repository popularity...', description: 'ProjectsPage: loading repository popularity', id: 'RwI3B9', }); const { isError, isLoading, data } = useGithubApi( /* * Repo should be defined for each project so for now it is safe for my * use-case. However, I should refactored it to handle cases where it is * not defined. The logic should be extracted in another component I think. * * TODO: fix this hardly readable argument */ meta.repos ? meta.repos.github ?? '' : '' ); if (isError) return 'Error'; if (isLoading || !data) return ; const getRepoPopularity = (repo: string) => { const stars = intl.formatMessage( { defaultMessage: '{starsCount, plural, =0 {No stars on Github} one {# star on Github} other {# stars on Github}}', description: 'ProjectsPage: Github stars count', id: 'sI7gJK', }, { starsCount: data.stargazers_count } ); const popularityUrl = `https://github.com/${repo}/stargazers`; return ( <> ⭐  {stars} ); }; const overviewMeta: (MetaItemData | undefined)[] = [ { id: 'creation-date', label: intl.formatMessage({ defaultMessage: 'Created on:', description: 'ProjectsPage: creation date label', id: 'wVFA4m', }), value: getDate(data.created_at), }, { id: 'update-date', label: intl.formatMessage({ defaultMessage: 'Updated on:', description: 'ProjectsPage: update date label', id: 'wQrvgw', }), value: getDate(data.updated_at), }, license ? { id: 'license', label: intl.formatMessage({ defaultMessage: 'License:', description: 'ProjectsPage: license label', id: 'VtYzuv', }), value: license, } : undefined, repos?.github ? { id: 'popularity', label: intl.formatMessage({ defaultMessage: 'Popularity:', description: 'ProjectsPage: popularity label', id: 'KrNvQi', }), value: getRepoPopularity(repos.github), } : undefined, repos ? { id: 'repositories', label: intl.formatMessage({ defaultMessage: 'Repositories:', description: 'ProjectsPage: repositories label', id: 'iDIKb7', }), value: getReposLinks(repos), } : undefined, technologies ? { id: 'technologies', label: intl.formatMessage({ defaultMessage: 'Technologies:', description: 'ProjectsPage: technologies label', id: 'RwNZ6p', }), value: technologies.map((techno) => { return { id: techno, value: techno }; }), } : undefined, ]; const filteredOverviewMeta = overviewMeta.filter( (item): item is MetaItemData => !!item ); const webpageSchema = getWebPageSchema({ description: seo.description, locale: website.locales.default, slug: asPath, title: seo.title, updateDate: dates.update, }); const articleSchema = getSinglePageSchema({ cover: `/projects/${id}.jpg`, dates, description: intro, id: 'project', kind: 'page', locale: website.locales.default, slug: asPath, title, }); const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]); return ( <> {page.title} {/*eslint-disable-next-line react/jsx-no-literals -- Name allowed */} {/*eslint-disable-next-line react/jsx-no-literals -- Content allowed */}